Skip to content

feat(comments): reactions + Wilson 'Best' sort (Tier 1)#1540

Open
marcusbellamyshaw-cell wants to merge 4 commits into
emdash-cms:mainfrom
marcusbellamyshaw-cell:feat/comment-reactions
Open

feat(comments): reactions + Wilson 'Best' sort (Tier 1)#1540
marcusbellamyshaw-cell wants to merge 4 commits into
emdash-cms:mainfrom
marcusbellamyshaw-cell:feat/comment-reactions

Conversation

@marcusbellamyshaw-cell

@marcusbellamyshaw-cell marcusbellamyshaw-cell commented Jun 18, 2026

Copy link
Copy Markdown

What does this PR do?

Adds comment reactions (positive-only "like") and a Wilson score "Best" sort to the <Comments> component. This is Tier 1 of the best-in-class comments RFC, approved by @ascorbic.

  • New _emdash_comment_reactions table (migration 044) — deduped per voter via a salted IP hash (same privacy primitive as comment ip_hash)
  • Public, honeypot- and rate-limited endpoint at GET/POST /_emdash/api/comments/:collection/:contentId/reactions
  • Two opt-in props on <Comments>: reactions (like button + live counts) and sort="best" (Wilson lower-bound ranking for top-level comments, replies stay chronological)
  • Progressive enhancement: the inline script is emitted only when reactions is enabled — pages without reactions ship zero additional JS
  • Fully additive: new table, new route, new optional props with behavior-preserving defaults

Closes #1497 (Tier 1)

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Sonnet 4.6 (claude-sonnet-4-6)

Screenshots / test output

Before — comments today (chronological, no reactions):

comments-before

After<Comments reactions sort="best" />:

comments-after

Test output: 118 tests pass, 15 new (Wilson math, toggle/dedupe, counts, rate-limit, best-sort ordering).

@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4b33efe

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Minor
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 1,012 lines across 16 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet cla: needed labels Jun 18, 2026
@marcusbellamyshaw-cell

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

marcusbellamyshaw-cell and others added 3 commits June 18, 2026 16:39
Tier 1 of the best-in-class comments RFC. Visitors can react (like) to
approved comments; reactions are stored first-party in a new
_emdash_comment_reactions table, deduped per voter by a salted IP hash,
and served via a public, honeypot- and rate-limited endpoint at
GET/POST /_emdash/api/comments/:collection/:contentId/reactions.

The <Comments> component gains two opt-in props: `reactions` (render a
like button + live counts) and `sort="best"` (Reddit-style Wilson
lower-bound ranking). Posting is progressively enhanced via a tiny inline
script emitted only when reactions are enabled. Additive and
backward-compatible: new table, new route, new optional props with
behavior-preserving defaults. Includes a changeset and tests.

AI disclosure: implemented with assistance from Claude (claude-opus-4-8);
all code verified against the repository and the full test suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gration

The reactions endpoint (GET/POST
/_emdash/api/comments/:collection/:contentId/reactions) needs an explicit
injectRoute entry in injectCoreRoutes — without it the path falls through to
the page handler. Verified end-to-end against the demo: toggle on/off and
aggregate counts return the expected JSON.

AI disclosure: implemented with assistance from Claude (claude-opus-4-8).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ucket limit

Adversarial self-review follow-ups on the reactions prototype:

- toggle() no longer does read-then-write. A concurrent double-toggle from the
  same voter could both see "no row" and have the second INSERT violate the
  unique index, surfacing as a 500. Now an idempotent INSERT ... ON CONFLICT
  DO NOTHING branches on whether a row was written, so it never throws.
- handleReactionToggle rejects reactions outside an allowlist (currently just
  "like", matching the shipped widget) so a caller can't spam arbitrary
  reaction strings and bloat a comment's count map.
- The public reactions route documents that the salted-IP voter hash collapses
  to a shared "unknown" bucket without a trusted IP (per-visitor dedup is the
  Tier 2 visitor-identity primitive), mirroring the comment ingest route.

Adds handler-level tests (happy path, unsupported reaction, non-approved and
missing comment, concurrent-toggle invariant). lint + typecheck clean; 82
comment tests pass.

AI disclosure: implemented with assistance from Claude (claude-opus-4-8).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

Comment thread .changeset/comment-reactions.md Outdated

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This is great

@pkg-pr-new

pkg-pr-new Bot commented Jun 19, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1540

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1540

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@1540

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1540

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1540

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@1540

emdash

npm i https://pkg.pr.new/emdash@1540

create-emdash

npm i https://pkg.pr.new/create-emdash@1540

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1540

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@1540

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@1540

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@1540

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@1540

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@1540

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1540

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1540

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1540

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1540

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1540

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1540

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@1540

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1540

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1540

commit: 9e370e2

@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/needs-review No maintainer or bot review yet labels Jun 19, 2026
@ascorbic ascorbic enabled auto-merge (squash) June 19, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants